package math.vectorSpace.R3;

import java.awt.Graphics2D;
import math.mathExtended.DoubleComparator;
import math.vectorSpace.R2.Point2D;
import math.vectorSpace.R3.visualization.Visualizable;
import math.vectorSpace.R3.visualization.Render3D;
import math.vectorSpace.R3.visualization.VisualizationProperties;

/**
 * Class for creating and working with points in R3.
 * Contains extensive methods for manipulating points.
 * 
 * @author Shimu Wu
 * @version 1.0
 */
public class Point3D implements Visualizable {

    private static final DoubleComparator comparator = new DoubleComparator();

    private double x, y, z;

    /***
     * Creates a point at origin.
     */
    public Point3D() {
        this(0, 0, 0);
    }

    /**
     * Creates a point at the specified x, y, z coordinate.
     * 
     * @param x the x-coordinate
     * @param y the y-coordinate
     * @param z the z-coordinate
     */
    public Point3D(double x, double y, double z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    /**
     * Creates a point at the location of the given point.
     * 
     * @param point a Point3D object
     */
    public Point3D(Point3D point) {
        this.x = point.x;
        this.y = point.y;
        this.z = point.z;
    }

    /**
     * Creates a point at the head of the given vector with the tail at the origin.
     * 
     * @param a {@link Vector3D} object
     */
    public Point3D(Vector3D vector) {
        this.x = vector.getX();
        this.y = vector.getY();
        this.z = vector.getZ();
    }

    /**
     * Returns true if this point is an invalid point, false otherwise.
     * An invalid point is returned from methods that cannot retrieve a
     * valid point. For example, if a method tries to find the point
     * of intersection between two lines, and is unable to, the method
     * would return an invalid point, which is simply created by
     * Point3D (Double.NaN, Double.NaN, Double.NaN)
     * 
     * @return true if this point is a null point, false otherwise
     */
    public boolean isInvalidPoint() {
        if (Double.isNaN(x)) {
            return true;
        }
        return false;
    }

    /**
     * Returns the x-coordinate of this point.
     * 
     * @return the x-coordinate of this point
     */
    public double getX() {
        return x;
    }

    /**
     * Returns the y-coordinate of this point.
     * 
     * @return the y-coordinate of this point
     */
    public double getY() {
        return y;
    }

    /**
     * Returns the z-coordinate of this point.
     * 
     * @return the z-coordinate of this point
     */
    public double getZ() {
        return z;
    }

    /**
     * Sets the x-coordinate of this point.
     * 
     * @param the x-coordinate
     */
    public void setX(double x) {
        this.x = x;
    }

    /**
     * Sets the y-coordinate of this point.
     * 
     * @param the y-coordinate
     */
    public void setY(double y) {
        this.y = y;
    }

    /**
     * Sets the z-coordinate of this point.
     * 
     * @param the z-coordinate
     */
    public void setZ(double z) {
        this.z = z;
    }

    /**
     * Sets the location of this point to the location of another point.
     * 
     * @param newPoint a Point3D
     */
    public void setPoint(Point3D newPoint) {
        setX(newPoint.getX());
        setY(newPoint.getY());
        setZ(newPoint.getZ());
    }

    /**
     * Sets the location of this point to the specified (x,y,z) coordinates.
     * 
     * @param x the x-coordinate
     * @param y the y-coordinate
     * @param z the z-coordinate
     */
    public void setPoint(double x, double y, double z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    /** 
     * Adds the coordinates of the given point to this point.
     * This method is useful for moving a point.
     * If moving only in one axis is required, the other coordinates 
     * can simply be set to zero. If subtracting is required simply 
     * set the coordinates to negative, the subtract method can also be used.
     * 
     * @param point a Point3D
     * @return a Point3D that represents the sum of this point and the
     *         given point
     * @see #add(Vector3D)
     * @see #subtract(Point3D)
     */
    public Point3D add(Point3D point) {
        return new Point3D(x + point.x, y + point.y, z + point.z);
    }

    public Point3D add(double xchange, double ychange, double zchange) {
        return new Point3D(x + xchange, y + ychange, z + zchange);
    }

    /** 
     * Adds the coordinates of the given point to this point.
     *  
     * @param point a Point3D
     * @return a Point3D that represents the sum of this point and the
     *         given point
     * @see #add(Point3D)
     */
    public Point3D add(Vector3D vector) {
        return new Point3D(x + vector.getX(), y + vector.getY(), z
                + vector.getZ());
    }

    /**
     * Subtracts the coordinates of the given point from this point.
     * 
     * @param point a Point3D
     * @return a Point3D that represents the difference between this point
     *         and the given point
     * @see #add(Point3D)
     *
     */
    public Point3D subtract(Point3D point) {
        return new Point3D(x - point.x, y - point.y, z - point.z);
    }

    /**
     * Subtracts the coordinates of the given point from this point.
     * 
     * @param point a Point3D
     * @return a Point3D that represents the difference between this point
     *         and the given point
     * @see #subtract(Point3D)
     * @see #add(Vector3D)
     */
    public Point3D subtract(Vector3D vector) {
        return (new Point3D(x - vector.getX(), y - vector.getY(), z
                - vector.getZ()));
    }

    /**
     * Checks if the given point is the same as this point.
     * 
     * @param point a Point3D
     * @return true if this point and the given point is the same, false otherwise
     */
    public boolean equal(Point3D point) {
        if (comparator.equal(x, point.x) && comparator.equal(y, point.y)
                && comparator.equal(z, point.z)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Checks if the given point is the same as this point.
     * 
     * @param point a Point3D object
     * @return true if this point and the given point is the same, false otherwise
     */
    public boolean on(Point3D point) {
        return equal(point);
    }

    /**
     * Returns true if this point lies on the given line
     * 
     * @param line a {@link Line3D} object
     * @return true if this point lies on the given line, false otherwise
     */
    public boolean on(Line3D line) {
        double LS = line.parameterAtX(x);
        double MS = line.parameterAtY(y);
        double RS = line.parameterAtZ(z);
        boolean LS_any_param = false;
        boolean MS_any_param = false;
        boolean RS_any_param = false;

        if (Double.isNaN(LS)) {
            LS_any_param = true;
        }
        if (Double.isNaN(MS)) {
            MS_any_param = true;
        }
        if (Double.isNaN(RS)) {
            RS_any_param = true;
        }

        // If any of the parameters is undefined, i.e. evaluates to Double.NaN
        // Then the if statements will evaluate Double.NaN to false
        // which makes sense in this context
        if (((LS == MS) || (LS_any_param || MS_any_param))
                && ((MS == RS) || (MS_any_param || RS_any_param))) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Returns true if a point is on the segment
     * 
     * @param point
     * @return
     */
    public boolean on(Segment3D segment) {
        if ((segment.withinBound(this)) && on(segment.convertToLine())) {
            return true;
        }
        return false;
    }

    /**
     * Returns true if this point lies on the given plane, false otherwise
     * 
     * @param plane a {@link Plane3D} object
     * @return true if this point is on the given plane, false otherwise
     */
    public boolean on(Plane3D plane) {
        if (comparator.equal(plane.getA() * x + plane.getB() * y
                + plane.getC() * z + plane.getD(), 0)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Returns the distance from this point to the origin.
     * 
     * @return the distance from this point to the origin
     */
    public double distance() {
        return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2));
    }

    /**
     * Returns the distance from this point to the given point.
     * 
     * @param point a Point3D object
     */
    public double distance(Point3D point) {
        return Math.sqrt(Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2)
                + Math.pow(z - point.z, 2));
    }

    /**
     *  Finds the distance between this point and a given line
     *  
     *  @param line a {@link Line3D} object
     *  @return the distance between this point and a given line
     */
    public double distance(Line3D line) {
        /*
        AB = direction vector of the line
        AP = vector formed by joining the point and a point on the line
        
        The magnitude of the cross product of AB and AP gives 
        the area of the parallelogram
        Divide the area by the magnitude of AB to give the height
        which also happens to be the distance between the line and the point
        (This solution was found on an online forum)
        
        P
        o------+------o
        \     |       \
        \    |h       \
        \   |         \
        ------o--+----------o-----------
        A             B
        
         */

        Vector3D vectorAP = new Vector3D(x - line.getRefPoint().getX(), y
                - line.getRefPoint().getY(), z - line.getRefPoint().getZ());
        double area = line.getDirVector().cross(vectorAP).length();
        return area / line.getDirVector().length();
    }

    /**
     * Returns the distance between this point and a given plane.
     * 
     * @param plane a {@link Plane3D} object
     * @return the distance between this point and the given plane
     */
    public double distance(Plane3D plane) {
        // Creates a vector from a point on the plane to the point 
        Vector3D tempVector = new Vector3D(plane.getRefPoint(), this);

        // The magnitude of the projection of tempVector onto the 
        // normal is the distance between the point and the plane
        return tempVector.projectMag(plane.getNormal());
    }

    /**
     *  Projects a given point onto this line
     *  
     *  @param line a Line3D object
     *  @return a Point3D representing the projection of this 
     *          point onto the given line
     */
    public Point3D project(Line3D line) {
        // Creates a plane using the direction vector of the line as 
        // its normal vector and uses this point as a reference point 
        // on the plane. The point where the plane and the line 
        // intersect will be the projection of the point onto the line

        Plane3D plane = new Plane3D(this, line.getDirVector());
        return (line.intersect(plane));
    }

    /**
     * Returns the projection of this point onto the surface of a given plane.
     * 
     * @param point a {@link Plane3D} object
     * @return a Point3D that represents the projection of this point onto the given plane
     */
    public Point3D project(Plane3D plane) {
        // Creates a line that is perpendicular to the plane and
        // includes the given point
        if (this.on(plane)) {
            return this;
        }
        Line3D line = new Line3D(this, plane.getNormal());

        // Find the point of intersection between the line and the plane
        // That point will be the projection of the point onto the plane
        return line.intersect(plane);
    }

    /**
     * Returns the String representation of this point of the form: Point3D (x, y, z).
     * 
     * @return the String representation of this point
     */
    @Override
    public String toString() {
        return ("Point3D (" + x + ", " + y + ", " + z + ")");
    }

    public boolean equals(Point3D point) {
        return (comparator.equal(this.x, point.x) && comparator.equal(this.y, point.y)
                && comparator.equal(this.z, point.z));
    }

    @Override
    public boolean equals(Object anotherObject) {
        if (anotherObject instanceof Point3D) {
            return equals((Point3D) anotherObject);
        }
        return false;
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 89 * hash + (int) (Double.doubleToLongBits(this.x) ^ (Double.doubleToLongBits(this.x) >>> 32));
        hash = 89 * hash + (int) (Double.doubleToLongBits(this.y) ^ (Double.doubleToLongBits(this.y) >>> 32));
        hash = 89 * hash + (int) (Double.doubleToLongBits(this.z) ^ (Double.doubleToLongBits(this.z) >>> 32));
        return hash;
    }

    @Override
    public void draw(Render3D render, Graphics2D graphics,
            VisualizationProperties properties) {
        draw(render, graphics, properties, 2.0);
    }

    /**
     * 
     * @param render
     * @param graphics
     * @param properties 
     * @param radius the radius of the point
     */
    public void draw(Render3D render, Graphics2D graphics,
            VisualizationProperties properties, double radius) {
        // Note this is the Ellipse2D from java.awt.geom.Ellipse2D 
        
        /*
        Point2D projection = render.project(this);
        graphics.setColor(properties.getFillColor());
        Graphics2DExtended.drawCircle(graphics, render.getScreenHeight(), 
                projection, radius);*/
        
        /*
        if (properties.isDisplayCoord()) {
            graphics.drawString("(" + String.format("%.4g%n", this.x)
                    + ", " + String.format("%.4g%n", this.y)
                    + ", " + String.format("%.4g%n", this.z) + ")",
                    (int) (ellipse.getX() + 4 + ellipse.getWidth()),
                    (int) (ellipse.getY() + 4 + ellipse.getWidth()));
        }*/
    }
}
